////////////////////////////////////////////////////////////////////////////////
//
//  FLEX USER GROUP HOKURIKU
//  Copyright 2009 Flex User Group Hokuriku
//  All Rights Reserved.
// 
//  NOTICE: Flex User Group Hokuriku permits you to use, 
//  modify, and distribute this file 
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package net.fxug.hokuriku.chocolat.service
{
    import flash.errors.IOError;
    import flash.events.DatagramSocketDataEvent;
    import flash.events.EventDispatcher;
    import flash.events.TimerEvent;
    import flash.net.DatagramSocket;
    import flash.utils.ByteArray;
    import flash.utils.Timer;

    import mx.logging.ILogger;

    import net.fxug.hokuriku.chocolat.events.SonarEvent;
    import net.fxug.hokuriku.chocolat.service.data.SocketServerData;
    import net.fxug.hokuriku.chocolat.utils.LoggingUtil;

    /**
     * スピーカーからの返答を受信(通信成功)
     * @eventType net.fxug.hokuriku.chocolat.events.SonarEvent
     */
    [Event(name="serverReplayRecieved", type="net.fxug.hokuriku.chocolat.events.SonarEvent")]

    /**
     * スピーカーが見つからなかった
     * @eventType net.fxug.hokuriku.chocolat.events.SonarEvent
     */
    [Event(name="serverNotDiscoverd", type="net.fxug.hokuriku.chocolat.events.SonarEvent")]

    /**
     * LAN環境内のスピーカーを見つける。オーディエンス用。
     *
     * [[使い方]]
     * <pre>
     * // スピーカーを探す
     * var sonar:Sonar = new Sonar(localAddress);
     *
     * sonar.addEventListener(SonarEvent.SERVER_REPLY_RECIEVED, this.serverReplyRecievedHandler);
     * sonar.addEventListener(SonarEvent.SERVER_NOT_DISCOVER, this.serverNotDiscoverdHandler);
     *
     * sonar.ping(remoteAddress);
     * </pre>
     *
     * @author mac
     */
    public class Sonar extends EventDispatcher
    {
        public static const SEARCH_LIMITE_TIME:int = 3 * 1000; // pingの返答がこの時間かえって来ない場合失敗とみなす

        public static const PORT:int = 33334; //オーディエンス側で受信に利用するポート

        public static const SEARCH_SPEAKER:String = "SEARCH_SPEAKER"; // スピーカーを探索

        private static var logger:ILogger = LoggingUtil.getLogger(Sonar);

        private static var datagramSocket:DatagramSocket; // close()してもポートが解放されない問題があるため、staticにして再利用

        private var pingTimer:Timer = new Timer(SEARCH_LIMITE_TIME, 1);

        private var localAddress:String;

        /**
         * コンストラクタ
         *
         * @param localAddress // IPアドレス
         */
        public function Sonar(localAddress:String)
        {
            logger.info("Sonar::new Sonnar Address = {0}", localAddress);
            this.localAddress = localAddress;
        }

        /**
         * pingを打つ
         *
         * @param remoteAddress
         */
        public function ping(remoteAddress:String):void
        {
            var message:ByteArray = new ByteArray();
            message.writeUTFBytes(SEARCH_SPEAKER);

            logger.info("Sonar::ping ip = {0} / port = {1}", remoteAddress, SonarReceiver.PORT);

            this.recieve();

            if (!this.isBound())
            {
                throw new Error("unconnected ping Error");
            }

            try
            {
                datagramSocket.send(message, 0, 0, remoteAddress, SonarReceiver.PORT);
            }
            catch (e:Error)
            {
                logger.error("    send faild IP = {0}", remoteAddress);

                datagramSocket.close();

                throw new Error("BroadCast Error");
            }

            // 一定時間経過後にpingが受信できなかったらエラーイベントをディスパッチする
            this.pingTimer.addEventListener(TimerEvent.TIMER_COMPLETE, pingTimerCompleteHandler);
            this.pingTimer.start();
        }

        /**
         * 待ちうけ開始
         */
        public function recieve():void
        {
            logger.info("Sonar::recieve 受信開始");

            if (!datagramSocket)
            {
                datagramSocket = new DatagramSocket();
                datagramSocket.bind(PORT, localAddress);
                logger.info("Sonar::Sonar bind local port = {0} / local address = {1}", datagramSocket.localPort, datagramSocket.localAddress);
            }

            datagramSocket.addEventListener(DatagramSocketDataEvent.DATA, dataReceivedEventHandler);
            datagramSocket.receive();
        }

        /**
         * バインドが成功しているかどうか
         *
         * @return
         */
        public function isBound():Boolean
        {
            return (datagramSocket && datagramSocket.bound);
        }

        /**
         * データを受信
         */
        private function dataReceivedEventHandler(event:DatagramSocketDataEvent):void
        {
            logger.debug("Sonar::dataReceivedHandler 受信");

            var message:String = event.data.readUTFBytes(event.data.bytesAvailable);
            if (message == SonarReceiver.REPLY_FROM_SPEAKER)
            {
                logger.info("    Received REPLY_FROM_SPEAKER from " + event.srcAddress + " to " + event.dstAddress + "> " + message);

                // スピーカーの存在を確認できたのでスピーカー情報をディスパッチ
                var serverReplyReceiveEvent:SonarEvent = new SonarEvent(SonarEvent.SERVER_REPLY_RECIEVED);
                serverReplyReceiveEvent.serverData = new SocketServerData(event.srcAddress, event.srcPort);

                this.dispatchEvent(serverReplyReceiveEvent);

                this.dispose();
            }
            else
            {
                // TODO エラーハンドリング
                logger.info("    undefined message recieved message = " + message);
            }
        }

        /**
         * スピーカーを発見できなかった
         */
        private function pingTimerCompleteHandler(event:TimerEvent):void
        {
            logger.warn("Sonar::pingTimerCompleteHandler");
            logger.warn("    スピーカーが見つかりませんでした");

            var sonarEvent:SonarEvent = new SonarEvent(SonarEvent.SERVER_NOT_DISCOVER);
            this.dispatchEvent(sonarEvent);

            this.dispose();
        }

        /**
         * 終了処理
         */
        public function dispose():void
        {
            logger.info("Sonar::dispose");

            if (this.pingTimer)
            {
                if (this.pingTimer.hasEventListener(TimerEvent.TIMER_COMPLETE))
                {
                    this.pingTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, pingTimerCompleteHandler);
                }
                this.pingTimer.stop();
            }

            if (datagramSocket)
            {
                if (datagramSocket.hasEventListener(DatagramSocketDataEvent.DATA))
                {
                    datagramSocket.removeEventListener(DatagramSocketDataEvent.DATA, dataReceivedEventHandler);
                }

                try
                {
                    datagramSocket.close();
                }
                catch (e:IOError)
                {
                    logger.error("    datagramSocket.close ERROR");
                }
            }
        }
    }
}

